media_pp\elements\sink\renderer\windows/d3d12_renderer.rs
1use std::{any::Any, sync::Arc};
2
3use crate::pp_log::{PpLog, pp_error, pp_info};
4use ffmpeg_next as ffmpeg;
5use thiserror::Error as ThisError;
6use windows::{
7 Win32::Graphics::Direct3D12::{ID3D12Device, ID3D12Fence, ID3D12Resource},
8 core::Interface,
9};
10
11use crate::{
12 buffer::MediaBuffer,
13 control::ControlMsg,
14 element::{Element, ElementType, Sink, element_pp_log},
15 elements::{SubmitError, filter::decoder::d3d12va_decoder::d3d12va_texture},
16 error::Result,
17 pool::UnboundObjectPoolRef,
18};
19
20/// One CPU-resident image plane — data pointer, byte length, and row
21/// stride. Deliberately a plain, GPU-vendor-agnostic struct (no
22/// dependency on any particular rendering crate's own type) — unlike
23/// [`D3d12FrameRenderer::submit_nv12_texture`]'s COM types, this is the
24/// one part of the trait that isn't D3D12-specific, so
25/// [`D3d12FrameRenderer`] implementors only need this crate to know
26/// anything about their concrete rendering setup for the CPU-upload path.
27#[derive(Clone, Copy)]
28pub struct RawPlane {
29 pub data: *const u8,
30 pub len: usize,
31 pub stride: usize,
32}
33
34/// What [`D3d12Renderer`] needs from an actual DX12 window/rendering
35/// implementation — deliberately the *only* thing this crate knows about
36/// D3D12 rendering. Not GPU-vendor-agnostic despite `submit_yuv420p`'s
37/// plain `RawPlane` args: `submit_nv12_texture`'s zero-copy path takes
38/// `ID3D12Resource`/`ID3D12Fence` directly, so this trait (and any
39/// element built on it) is inherently D3D12-only — a Vulkan/CUDA renderer
40/// would need its own trait, not an impl of this one. `D3d12Renderer`
41/// itself only depends on this trait (plus the `windows` COM types the
42/// zero-copy path needs to pass through) — not on `renderer_engine` or
43/// any other concrete rendering crate. A caller wanting to actually
44/// render implements this for its own window/rendering stack; this
45/// repository's examples use `examples/render/render_common` for that
46/// implementation, outside the `media-pp` crate itself.
47pub trait D3d12FrameRenderer: Send {
48 /// The `ID3D12Device` this implementation actually renders/submits
49 /// with. [`D3d12Renderer`] reads this once at construction to guard
50 /// [`D3d12FrameRenderer::submit_nv12_texture`]'s zero-copy path: a
51 /// texture from a different device is invalid to draw from at all
52 /// (not just wrong-looking), so it's checked against this rather than
53 /// trusted.
54 fn device(&self) -> ID3D12Device;
55
56 /// # Safety
57 /// All plane pointers must be readable for the given length and
58 /// remain valid until this call returns.
59 unsafe fn submit_yuv420p(
60 &self,
61 y: RawPlane,
62 u: RawPlane,
63 v: RawPlane,
64 width: u32,
65 height: u32,
66 ) -> std::result::Result<(), SubmitError>;
67
68 /// # Safety
69 /// `texture` must be a valid `ID3D12Resource` on the same
70 /// `ID3D12Device` this renderer was created with, laid out as NV12.
71 /// `fence` must only reach `fence_value` once the GPU work that
72 /// produced `texture`'s contents has completed.
73 unsafe fn submit_nv12_texture(
74 &self,
75 texture: ID3D12Resource,
76 fence: ID3D12Fence,
77 fence_value: u64,
78 width: u32,
79 height: u32,
80 keep_alive: Box<dyn Any + Send>,
81 ) -> std::result::Result<(), SubmitError>;
82
83 fn resize(&self, width: u32, height: u32) -> std::result::Result<(), SubmitError>;
84}
85
86/// Errors specific to `D3d12Renderer`. Converts into the crate-wide `Error`
87/// via `?` (see [`crate::error::Error`]).
88#[derive(Debug, ThisError)]
89pub enum D3d12RendererError {
90 #[error("failed to submit frame: {0:?}")]
91 Submit(SubmitError),
92
93 #[error("failed to resize: {0:?}")]
94 Resize(SubmitError),
95
96 #[error(
97 "D3d12Renderer only handles YUV420P frames (CPU) or D3D12 frames \
98 (from D3d12vaDecoder), got {0:?}"
99 )]
100 UnsupportedFormat(ffmpeg::format::Pixel),
101
102 #[error(
103 "frame claimed the D3D12 pixel format but has no AVD3D12VAFrame \
104 payload — must come from D3d12vaDecoder"
105 )]
106 InvalidD3d12Frame,
107
108 #[error(
109 "a Pixel::D3D12 frame's texture lives on a different ID3D12Device \
110 than this D3d12Renderer was created with — the producer \
111 (D3d12vaDecoder/D3d12Upload) and the D3d12FrameRenderer impl \
112 must share the same device for zero-copy to be valid"
113 )]
114 DeviceMismatch,
115}
116
117/// Terminal sink that submits decoded video frames to a caller-supplied
118/// [`D3d12FrameRenderer`]. Only built with the `d3d12` feature —
119/// every consumer that doesn't need to render to a window pulls in
120/// neither this nor the `windows` dependency it needs for the zero-copy
121/// path.
122///
123/// Handles two kinds of input, dispatched on `frame.format()`:
124/// - `Pixel::YUV420P`: CPU-decoded (e.g. from `SwDecoder`) — copies
125/// pixel bytes to the GPU via `D3d12FrameRenderer::submit_yuv420p`.
126/// - `Pixel::D3D12`: GPU-decoded (from `D3d12vaDecoder`) — zero-copy,
127/// draws straight from the decoder's own texture via
128/// `D3d12FrameRenderer::submit_nv12_texture`.
129pub struct D3d12Renderer {
130 pp_log: PpLog,
131 name: Arc<str>,
132 inner: Box<dyn D3d12FrameRenderer>,
133 /// Captured once from `inner.device()` at construction — the
134 /// reference `submit_d3d12_frame` checks every zero-copy frame's
135 /// actual device against. Fetched from `inner` itself rather than
136 /// taken as a separate constructor parameter: a second
137 /// independently-supplied device would just be another value the
138 /// caller could get wrong, proving nothing about what `inner` really
139 /// renders with.
140 device: ID3D12Device,
141}
142
143impl D3d12Renderer {
144 /// `renderer` is whatever the caller's own [`D3d12FrameRenderer`]
145 /// implementation is — already constructed and pointed at a real
146 /// window/device by the time it gets here. This element doesn't
147 /// create or own a window itself.
148 pub fn new(name: impl Into<String>, renderer: Box<dyn D3d12FrameRenderer>) -> Self {
149 let name: Arc<str> = name.into().into();
150 let pp_log = element_pp_log(ElementType::D3d12Renderer, &name, None);
151 pp_info!(pp_log: &pp_log, "created");
152 let device = renderer.device();
153 Self {
154 name,
155 pp_log,
156 inner: renderer,
157 device,
158 }
159 }
160
161 /// Call when the target window resizes.
162 pub fn resize(&self, width: u32, height: u32) -> Result<()> {
163 self.inner
164 .resize(width, height)
165 .inspect_err(|error| pp_error!(self, "resize failed: {error:?}"))
166 .map_err(D3d12RendererError::Resize)?;
167 pp_info!(self, "resized: {width}x{height}");
168 Ok(())
169 }
170
171 fn submit_yuv420p_frame(&self, frame: &ffmpeg::frame::Video) -> Result<()> {
172 let plane = |index: usize| RawPlane {
173 data: frame.data(index).as_ptr(),
174 len: frame.data(index).len(),
175 stride: frame.stride(index),
176 };
177
178 // Safety: `plane(0..3)` point into `frame`'s own buffers, which
179 // outlive this call — `submit_yuv420p` only reads them before
180 // returning.
181 unsafe {
182 self.inner
183 .submit_yuv420p(plane(0), plane(1), plane(2), frame.width(), frame.height())
184 .map_err(D3d12RendererError::Submit)?;
185 }
186 Ok(())
187 }
188
189 fn submit_d3d12_frame(
190 &self,
191 frame: Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
192 ) -> Result<()> {
193 let (texture_raw, fence_raw, fence_value) =
194 d3d12va_texture(&frame).ok_or(D3d12RendererError::InvalidD3d12Frame)?;
195 let width = frame.width();
196 let height = frame.height();
197
198 // Safety: `texture_raw`/`fence_raw` are borrowed raw COM pointers
199 // — still owned by `frame`'s own hw frame pool reference, not by
200 // us. `.clone()` (`AddRef`) gives us our own independently
201 // ref-counted handle, valid for as long as *we* hold it,
202 // regardless of what `frame`/ffmpeg later does with its copy.
203 let (texture, fence) = unsafe {
204 let texture = ID3D12Resource::from_raw_borrowed(&texture_raw)
205 .expect("AVD3D12VAFrame.texture must not be null")
206 .clone();
207 let fence = ID3D12Fence::from_raw_borrowed(&fence_raw)
208 .expect("AVD3D12VAFrame.sync_ctx.fence must not be null")
209 .clone();
210 (texture, fence)
211 };
212
213 // The producer (`D3d12vaDecoder`/`D3d12Upload`) and `self.inner`
214 // are independent constructions that only *should* share a
215 // device by convention — verify it, since drawing a different
216 // device's texture is invalid, not just wrong output.
217 let mut texture_device: Option<ID3D12Device> = None;
218 unsafe { texture.GetDevice(&mut texture_device) }
219 .map_err(|_| D3d12RendererError::DeviceMismatch)?;
220 let texture_device = texture_device.ok_or(D3d12RendererError::DeviceMismatch)?;
221 if texture_device.as_raw() != self.device.as_raw() {
222 return Err(D3d12RendererError::DeviceMismatch.into());
223 }
224
225 // `frame` (an `Arc`) is what keeps the underlying D3D12 texture
226 // memory from being recycled by the decoder's frame pool while
227 // the renderer still has it queued to draw — independent of, and
228 // in addition to, the `texture`/`fence` COM references above.
229 unsafe {
230 self.inner
231 .submit_nv12_texture(texture, fence, fence_value, width, height, Box::new(frame))
232 .map_err(D3d12RendererError::Submit)?;
233 }
234 Ok(())
235 }
236}
237
238impl Element for D3d12Renderer {
239 fn name(&self) -> Arc<str> {
240 self.name.clone()
241 }
242
243 fn element_type(&self) -> ElementType {
244 ElementType::D3d12Renderer
245 }
246
247 fn pp_log(&self) -> &PpLog {
248 &self.pp_log
249 }
250
251 fn pp_log_mut(&mut self) -> &mut PpLog {
252 &mut self.pp_log
253 }
254}
255
256impl Sink for D3d12Renderer {
257 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
258 let MediaBuffer::Video(frame) = buf else {
259 return Ok(());
260 };
261
262 match frame.format() {
263 ffmpeg::format::Pixel::YUV420P => self
264 .submit_yuv420p_frame(&frame)
265 .inspect_err(|error| pp_error!(self, "submit_yuv420p_frame failed: {error}")),
266 ffmpeg::format::Pixel::D3D12 => self
267 .submit_d3d12_frame(frame)
268 .inspect_err(|error| pp_error!(self, "submit_d3d12_frame failed: {error}")),
269 other => {
270 pp_error!(self, "unsupported pixel format: {other:?}");
271 Err(D3d12RendererError::UnsupportedFormat(other).into())
272 }
273 }
274 }
275
276 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
277 // Terminal, nothing to flush or forward — a paused/stopped window
278 // just stops receiving new frames (see `Queue`'s worker loop) and
279 // keeps showing whatever was submitted last.
280 Ok(())
281 }
282}